package com.aloudmeta.grpc.support.interceptors;

import com.aloudmeta.grpc.support.check.GrpcSecurityMetadataSource;
import io.grpc.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.rpc.protocol.grpc.interceptors.ServerInterceptor;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.SecurityMetadataSource;
import org.springframework.security.access.intercept.AbstractSecurityInterceptor;
import org.springframework.security.access.intercept.InterceptorStatusToken;
import org.springframework.security.core.AuthenticationException;

import static org.apache.dubbo.common.constants.CommonConstants.PROVIDER;

/**
 * A server interceptor that will check the security context whether it has permission to access the grpc method. This
 * interceptor uses a {@link GrpcSecurityMetadataSource} to obtain the information how the called method is protected
 * and uses an {@link AccessDecisionManager} to evaluate that information. This interceptor isn't needed if you use
 * spring's security annotations, but can be used additionally. An example use case of using both would be requiring all
 * users to be authenticated, while using the annotations to require further permissions.
 *
 * <p>
 * <b>Note:</b> If you use spring's security annotations, the you have to use
 * {@code @EnableGlobalMethodSecurity(proxyTargetClass = true, ...)}
 * </p>
 *
 */
@Slf4j
@Activate(group = {PROVIDER}, order=5100)
public class AuthorizationCheckingServerInterceptor extends AbstractSecurityInterceptor implements ServerInterceptor {

    private GrpcSecurityMetadataSource grpcSecurityMetadataSource;

    public void setGrpcSecurityMetadataSource(GrpcSecurityMetadataSource grpcSecurityMetadataSource) {
        this.grpcSecurityMetadataSource = grpcSecurityMetadataSource;
    }

    @SuppressWarnings("unchecked")
    @Override
    public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(final ServerCall<ReqT, RespT> call, final Metadata headers,
                                                                 final ServerCallHandler<ReqT, RespT> next) {
        final MethodDescriptor<ReqT, RespT> methodDescriptor = call.getMethodDescriptor();
        final InterceptorStatusToken token;
        try {
            token = beforeInvocation(methodDescriptor);
        } catch (final AuthenticationException | AccessDeniedException e) {
            log.debug("Access denied");
            throw e;
        }
        log.debug("Access granted");
        final ServerCall.Listener<ReqT> result;
        try {
            result = next.startCall(call, headers);
        } finally {
            finallyInvocation(token);
        }
        // TODO: Call that here or in onHalfClose?
        return (ServerCall.Listener<ReqT>) afterInvocation(token, result);
    }

    @Override
    public Class<?> getSecureObjectClass() {
        return MethodDescriptor.class;
    }

    @Override
    public SecurityMetadataSource obtainSecurityMetadataSource() {
        return this.grpcSecurityMetadataSource;
    }

}
